#[macro_use]
extern crate lazy_static;
use macroquad::audio::{load_sound, play_sound, play_sound_once, PlaySoundParams, Sound};
use macroquad::prelude::*;
use rlua::{Context, Function, Lua, Result};
use std::collections::{HashMap, HashSet};
use std::env;
use std::ffi::OsStr;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Mutex;

use notify::{watcher, RecursiveMode, Watcher};
use std::sync::mpsc::channel;
use std::time::{Duration, Instant, SystemTime};

mod keys;

lazy_static! {
    static ref BASE_PATH: PathBuf = env::current_dir().unwrap();
    static ref IMAGE: Mutex<Image> = Mutex::new(Image::gen_image_color(
        screen_width() as u16,
        screen_height() as u16,
        BLACK
    ));
    static ref FONTS: Mutex<HashMap<String, Font>> = Mutex::new(HashMap::new());
    static ref TEXTURES: Mutex<HashMap<String, Texture2D>> = Mutex::new(HashMap::new());
    static ref UNLOADED_TEXTURES: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
    static ref SOUNDS: Mutex<HashMap<String, Sound>> = Mutex::new(HashMap::new());
    static ref UNLOADED_SOUNDS: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
    static ref MOUSE: Mutex<(f32, f32)> = Mutex::new((0., 0.));
}

fn window_conf() -> Conf {
    Conf {
        window_title: "FEN2".to_owned(),
        window_width: 640,
        window_height: 480,
        ..Default::default()
    }
}

// user code will load images into the TEXTURES dict by path name
async fn _preload_texture(path: String, reload: bool) -> bool {
    let mut textures = TEXTURES.lock().unwrap();

    let p = &path;
    if reload || !textures.contains_key(p) {
        match load_texture(p).await {
            Ok(t) => {
                textures.insert(path, t);
                return true;
            }
            Err(e) => {
                println!("{:?}", e);
                return false;
            }
        }
    } else {
        return true;
    }
}

async fn _handle_unloaded_textures() {
    for s in UNLOADED_TEXTURES.lock().unwrap().drain() {
        println!("loading {:?}", s);
        _preload_texture(s, false).await;
    }
}

fn _preload_texture_sync(_: Context, path: String) -> Result<()> {
    UNLOADED_TEXTURES.lock().unwrap().insert(path);
    Ok(())
}

async fn _preload_sound(path: String, reload: bool) -> bool {
    let mut sounds = SOUNDS.lock().unwrap();

    let p = &path;
    if reload || !sounds.contains_key(p) {
        match load_sound(p).await {
            Ok(t) => {
                sounds.insert(path, t);
                return true;
            }
            Err(e) => {
                println!("{:?}", e);
                return false;
            }
        }
    } else {
        return true;
    }
}

async fn _handle_unloaded_sounds() {
    for s in UNLOADED_SOUNDS.lock().unwrap().drain() {
        println!("loading {:?}", s);
        _preload_sound(s, false).await;
    }
}

fn _preload_sound_sync(_: Context, path: String) -> Result<()> {
    UNLOADED_SOUNDS.lock().unwrap().insert(path);
    Ok(())
}

fn _play_sound(_: Context, (path, looped, volume): (String, bool, f32)) -> Result<()> {
    let sounds = SOUNDS.lock().unwrap();
    match sounds.get(&path) {
        Some(t) => play_sound(
            *t,
            PlaySoundParams {
                looped: looped,
                volume: volume,
            },
        ),
        None => println!("Error: no sound {}", &path),
    }
    Ok(())
}

fn _draw_texture(_: Context, (path, x, y): (String, i32, i32)) -> Result<()> {
    let textures = TEXTURES.lock().unwrap();
    match textures.get(&path) {
        Some(t) => draw_texture(*t, x as f32, y as f32, WHITE),
        None => println!("Error: no texture {}", &path),
    }
    Ok(())
}

fn _draw_texture_ex(
    _: Context,
    (path, x, y, sx, sy, sw, sh): (String, i32, i32, i32, i32, i32, i32),
) -> Result<()> {
    let textures = TEXTURES.lock().unwrap();
    match textures.get(&path) {
        Some(t) => draw_texture_ex(
            *t,
            x as f32,
            y as f32,
            WHITE,
            DrawTextureParams {
                source: Some(Rect {
                    x: sx as f32,
                    y: sy as f32,
                    w: sw as f32,
                    h: sh as f32,
                }),
                ..Default::default()
            },
        ),
        None => println!("Error: no texture {}", &path),
    }
    Ok(())
}

fn set_pixel(ctx: Context, (x, y, c): (u32, u32, bool)) -> Result<()> {
    IMAGE
        .lock()
        .unwrap()
        .set_pixel(x, y, if c { BLACK } else { WHITE });
    Ok(())
}

fn draw_text(_: Context, (s, x, y, c): (String, i32, i32, bool)) -> Result<()> {
    let font = *FONTS
        .lock()
        .unwrap()
        .get(&String::from("HelvetiPixel"))
        .unwrap();
    draw_text_ex(
        &s,
        x as f32,
        y as f32,
        TextParams {
            font: font,
            font_size: 16,
            font_scale: 1.0,
            font_scale_aspect: 1.0,
            color: if c { BLACK } else { WHITE },
            ..Default::default()
        },
    );
    Ok(())
}

fn _draw_rect(_: Context, (x, y, w, h, c): (i32, i32, i32, i32, bool)) -> Result<()> {
    draw_rectangle(
        x as f32,
        y as f32,
        w as f32,
        h as f32,
        if c { BLACK } else { WHITE },
    );
    Ok(())
}

fn _draw_rect_lines(
    _: Context,
    (x, y, w, h, thickness, c): (i32, i32, i32, i32, i32, bool),
) -> Result<()> {
    draw_rectangle_lines(
        x as f32,
        y as f32,
        w as f32,
        h as f32,
        (thickness * 2) as f32,
        if c { BLACK } else { WHITE },
    );
    Ok(())
}

fn int_to_button(i: i32) -> MouseButton {
    match i {
        0 => MouseButton::Right,
        1 => MouseButton::Left,
        2 => MouseButton::Middle,
        _ => MouseButton::Unknown,
    }
}

fn _mouse_pos(_: Context, (): ()) -> Result<(i32, i32)> {
    let (x, y) = *MOUSE.lock().unwrap();
    return Ok((x as i32, y as i32));
}

fn _show_mouse(_: Context, show: bool) -> Result<()> {
    show_mouse(show);
    Ok(())
}

fn _mouse_down(_: Context, btn: i32) -> Result<bool> {
    return Ok(is_mouse_button_down(int_to_button(btn)));
}

fn _mouse_pressed(_: Context, btn: i32) -> Result<bool> {
    return Ok(is_mouse_button_pressed(int_to_button(btn)));
}

fn _mouse_released(_: Context, btn: i32) -> Result<bool> {
    return Ok(is_mouse_button_released(int_to_button(btn)));
}

fn _key_down(_: Context, key: String) -> Result<bool> {
    return Ok(is_key_down(keys::keycode(&key)));
}

fn _key_pressed(_: Context, key: String) -> Result<bool> {
    return Ok(is_key_pressed(keys::keycode(&key)));
}

fn _key_released(_: Context, key: String) -> Result<bool> {
    return Ok(is_key_released(keys::keycode(&key)));
}

fn print_lua_error(e: &rlua::Error) {
    match e {
        rlua::Error::RuntimeError(s) => {
            for s in format!("{:?}", e).split("\\n") {
                println!("{}", s)
            }
        }
        other => println!("{:?}", other),
    }
}

struct App<'a> {
    root: &'a Path,
    lua: Lua,
}

impl<'a> App<'a> {
    unsafe fn new(root: &'a Path) -> App<'a> {
        env::set_current_dir(&root).unwrap();
        let lua = Lua::new_with_debug();
        lua.context(|lua_ctx| {
            let _G = lua_ctx.globals();
            _G.set("set_pixel", lua_ctx.create_function(set_pixel).unwrap())
                .unwrap();
            _G.set("draw_text", lua_ctx.create_function(draw_text).unwrap())
                .unwrap();
            _G.set(
                "load_img",
                lua_ctx.create_function(_preload_texture_sync).unwrap(),
            )
            .unwrap();
            _G.set("draw_img", lua_ctx.create_function(_draw_texture).unwrap())
                .unwrap();
            _G.set(
                "draw_sprite",
                lua_ctx.create_function(_draw_texture_ex).unwrap(),
            )
            .unwrap();

            _G.set(
                "load_sound",
                lua_ctx.create_function(_preload_sound_sync).unwrap(),
            )
            .unwrap();
            _G.set("play_sound", lua_ctx.create_function(_play_sound).unwrap())
                .unwrap();

            _G.set("draw_rect", lua_ctx.create_function(_draw_rect).unwrap())
                .unwrap();
            _G.set(
                "draw_rect_lines",
                lua_ctx.create_function(_draw_rect_lines).unwrap(),
            )
            .unwrap();

            _G.set("show_mouse", lua_ctx.create_function(_show_mouse).unwrap())
                .unwrap();
            _G.set("mouse_pos", lua_ctx.create_function(_mouse_pos).unwrap())
                .unwrap();
            _G.set("mouse_down", lua_ctx.create_function(_mouse_down).unwrap())
                .unwrap();
            _G.set(
                "mouse_pressed",
                lua_ctx.create_function(_mouse_pressed).unwrap(),
            )
            .unwrap();
            _G.set(
                "mouse_released",
                lua_ctx.create_function(_mouse_released).unwrap(),
            )
            .unwrap();
            _G.set("key_down", lua_ctx.create_function(_key_down).unwrap())
                .unwrap();
            _G.set(
                "key_pressed",
                lua_ctx.create_function(_key_pressed).unwrap(),
            )
            .unwrap();
            _G.set(
                "key_released",
                lua_ctx.create_function(_key_released).unwrap(),
            )
            .unwrap();

            // both package.path and fennel.path use '?' as wildcard
            let mut base_copy = BASE_PATH.clone();
            base_copy.push("?");

            lua_ctx
                .load(
                    std::format!(
                        "package.path = package.path .. ';{}.lua'",
                        str::replace(base_copy.to_str().unwrap(), "\\", "\\\\")
                    )
                    .as_str(),
                )
                .exec()
                .unwrap();

            lua_ctx.load("fennel = require('fennel')").exec().unwrap();
            lua_ctx
                .load("table.insert(package.loaders or package.searchers, fennel.searcher)")
                .exec()
                .unwrap();

            lua_ctx
                .load(
                    std::format!(
                        "fennel.path = fennel.path .. ';{}.fnl'",
                        str::replace(base_copy.to_str().unwrap(), "\\", "\\\\")
                    )
                    .as_str(),
                )
                .exec()
                .unwrap();

            lua_ctx.load("system = require('system')").exec().unwrap();

            match lua_ctx.load("app = require(\"app\")").exec() {
                Err(e) => print_lua_error(&e),
                Ok(()) => (),
            }
        });

        App {
            lua: lua,
            root: root,
        }
    }
    fn execute(&self, s: &str) {
        self.lua.context(|lua_ctx| match lua_ctx.load(s).exec() {
            Err(e) => print_lua_error(&e),
            Ok(()) => (),
        })
    }
    fn set_working_directory(&self) {
        env::set_current_dir(&self.root).unwrap();
    }
    async fn reload(&self, path: PathBuf) {
        let path_s = path.to_str().unwrap();
        let root_s = self.root.to_str().unwrap();
        let mime = path.extension().and_then(OsStr::to_str);
        // strip the app's root
        match path_s.strip_prefix(root_s) {
            Some(_s) => {
                let s = _s.strip_prefix(&"\\").unwrap_or(_s);
                println!("{:?} changed", s);
                match mime {
                    Some("png") | Some("bmp") => {
                        _preload_texture(s.to_string(), true).await;
                    }
                    Some("lua") | Some("fnl") => {
                        self.execute(&format!("system.reload_path({:?})", s));
                        // use the normal require to update our `app` binding
                        self.execute(&"app = require(\"app\")");
                    }
                    _ => (),
                }
            }
            None => (),
        }
    }
    fn update(&self, dt: f64) {
        self.execute(&format!("if app.update then app.update({}) end", dt));
    }
}

#[macroquad::main(window_conf)]
async fn main() {
    // we'll time our own delta time
    let instant = Instant::now();
    let mut elapsed = instant.elapsed().as_secs_f64();

    let (tx, rx) = channel();
    let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap();
    watcher.watch("files", RecursiveMode::Recursive).unwrap();

    FONTS.lock().unwrap().insert(
        String::from("HelvetiPixel"),
        load_ttf_font("./HelvetiPixel.ttf").await.unwrap(),
    );

    //let texture = Texture2D::from_image(&*IMAGE.lock().unwrap());

    // eventually we'll scan for available apps
    let mut app_root = BASE_PATH.clone();
    app_root.push("files");
    app_root.push("testapp");
    //rlua considers lua with debug to be unsafe

    let render = render_target(640, 480);
    render.texture.set_filter(FilterMode::Nearest);
    let mut render_cam = Camera2D::from_display_rect(Rect {
        x: 0.,
        y: 0.,
        w: 640.,
        h: 480.,
    });

    render_cam.render_target = Some(render);
    render_cam.zoom.y = render_cam.zoom.y * -1.;

    unsafe {
        let app = App::new(app_root.as_path());

        loop {
            let (width, height, scale) = if screen_width() >= 1280. && screen_height() >= 960. {
                (1280., 960., 2.)
            } else if screen_width() >= 640. && screen_height() >= 480. {
                (640., 480., 1.)
            } else {
                (320., 240., 0.5)
            };
            let (mx, my) = mouse_position();

            *MOUSE.lock().unwrap() = (
                ((mx - ((screen_width() - width) * 0.5)) / scale).round(),
                ((my - ((screen_height() - height) * 0.5)) / scale).round(),
            );

            set_camera(&render_cam);

            app.set_working_directory();
            _handle_unloaded_textures().await;
            _handle_unloaded_sounds().await;
            clear_background(WHITE);

            let dt = instant.elapsed().as_secs_f64() - elapsed;
            elapsed = instant.elapsed().as_secs_f64();
            clear_background(LIGHTGRAY);
            app.update(dt);

            //texture.update(&*IMAGE.lock().unwrap());
            //draw_texture(texture, 0., 0., WHITE);

            set_default_camera();
            clear_background(BLACK);

            draw_texture_ex(
                render.texture,
                ((screen_width() - width) / 2.).floor(),
                ((screen_height() - height) / 2.).floor(),
                WHITE,
                DrawTextureParams {
                    dest_size: Some(vec2(width, height)),
                    ..Default::default()
                },
            );

            match rx.try_recv() {
                Ok(notify::DebouncedEvent::NoticeWrite(path)) => {
                    app.reload(path).await;
                }
                Err(e) => (),
                _ => (),
            }

            env::set_current_dir(BASE_PATH.clone()).unwrap();

            next_frame().await
        }
    }
}
